﻿using System;
using System.Collections.Generic;
using Test30.Core;

namespace Test30.Engine
{
    public class Maze
    {
        public Rect Bounds => new Rect(0, 0, _cells.Width - 1, _cells.Height - 1);

        public Maze(int width, int height)
        {
            // pad by one for the outer bottom and right walls
            _cells = new Array2D<Cell>(width + 1, height + 1);

            _cells.Fill((pos) => new Cell());
        }

        public void GrowTree()
        {
            List<Vec> cells = new List<Vec>();

            // start with a random cell
            Vec pos = Rng.Vec(Bounds);

            Open(pos);
            cells.Add(pos);

            while (cells.Count > 0)
            {
                // weighting how the index is chosen here will affect the way the
                // maze looks. see the function description
                int index = Math.Abs(Rng.TriangleInt(0, cells.Count - 1));
                Vec cell = cells[index];

                // see which adjacent cells are open
                List<Direction> unmadeCells = new List<Direction>();

                if (CanCarve(cell, Direction.N)) unmadeCells.Add(Direction.N);
                if (CanCarve(cell, Direction.S)) unmadeCells.Add(Direction.S);
                if (CanCarve(cell, Direction.E)) unmadeCells.Add(Direction.E);
                if (CanCarve(cell, Direction.W)) unmadeCells.Add(Direction.W);

                if (unmadeCells.Count > 0)
                {
                    Direction direction = Rng.Item(unmadeCells);

                    Carve(cell, direction);

                    cells.Add(cell + direction);
                }
                else
                {
                    // no adjacent uncarved cells
                    cells.RemoveAt(index);
                }
            }
        }
        private bool CanCarve(Vec pos, Direction direction)
        {
            // must start in bounds
            if (!Bounds.Contains(pos)) return false;

            // must end in bounds
            if (!Bounds.Contains(pos + direction)) return false;

            // destination must not be open
            if (_cells[pos + direction].IsOpen) return false;

            return true;
        }

        private void Carve(Vec pos, Direction direction)
        {
            if (!Bounds.Contains(pos)) throw new ArgumentOutOfRangeException(nameof(pos));

            // open the destination
            if (Bounds.Contains(pos + direction))
            {
                _cells[pos + direction].IsOpen = true;
            }

            // cut the wall
            if (direction == Direction.N) _cells[pos].IsTopWallOpen = true;
            else if (direction == Direction.S) _cells[pos + direction].IsTopWallOpen = true;
            else if (direction == Direction.W) _cells[pos].IsLeftWallOpen = true;
            else if (direction == Direction.E) _cells[pos + direction].IsLeftWallOpen = true;
            else throw new ArgumentException("The direction must be one of N, S, E, or W.");
        }

        private void Open(Vec pos)
        {
            if (!Bounds.Contains(pos)) throw new ArgumentOutOfRangeException(nameof(pos));

            _cells[pos].IsOpen = true;
        }


        public void Draw(Action<Vec> carveOpening)
        {
            foreach (Vec pos in _cells.Bounds)
            {
                // valid cell in the maze

                // open the cell
                if (Bounds.Contains(pos) && _cells[pos].IsOpen)
                {
                    Vec tile = (pos * 2) + 1;
                    carveOpening(tile);
                }

                // open the left wall
                if ((pos.Y < Bounds.Height) && _cells[pos].IsLeftWallOpen)
                {
                    Vec tile = (pos * 2) + new Vec(0, 1);
                    carveOpening(tile);
                }

                // open the top wall
                if ((pos.X < Bounds.Width) && _cells[pos].IsTopWallOpen)
                {
                    Vec tile = (pos * 2) + new Vec(1, 0);
                    carveOpening(tile);
                }
            }
        }

        public void Draw(Array2D<Tile> tiles)
        {
            Draw(pos => tiles[pos].Type = TileType.Floor);
        }

        private readonly Array2D<Cell> _cells;

        private class Cell
        {
            public bool IsOpen;
            public bool IsLeftWallOpen;
            public bool IsTopWallOpen;
        }
    }
}
